<?php
/**
 * Magic object class to handle the data and build a standard object that inherits to all subclasses
 *
 * @author Michael Ruf
 */
class ZeusLib_Object extends ZeusLib_Cache_Abstract implements IteratorAggregate
{

	/**
	 * Object attributes
	 *
	 * @var array
	 */
	protected $data = array();

	/**
	 * Data changed flag (true after setData|unsetData call)
	 *
	 * @var bool
	 */
	protected $hasDataChanged = false;

	/**
	 * Underscore cache to reduce using preg_replace
	 *
	 * @var array
	 */
	private static $_underscoreCache = array();


	/**
	 * Constructor
	 * By default is looking for first argument as array and assignes it as object attributes
	 *
	 * @param [array] $data
	 */
	public function __construct(array $data = array())
	{
		// Call the cache constructor
		parent::__construct();

		// Check whether data was given
		if (!empty($data))
		{
			// Initialize data
			$this->data = $data;
		}

		// Call internal constructor
		$this->_construct();

		// Return instance
		return $this;
	}

	/**
	 * Internal constructor.
	 * Can be used for object initialization.
	 */
	protected function _construct()
	{
	}

	/**
	 * Adds data to the object.
	 *
	 * Overwrites previous data in the object with the same index.
	 *
	 * @param array $data
	 * @return ZeusLib_Object
	 */
	public function addData(array $data)
	{
		// Iterate the given data array
		foreach($data as $key => $value)
		{
			// Set/overwrite existing data
			$this->setData($key, $value);
		}

		// Return instance
		return $this;
	}

	/**
	 * Sets/overwrites data in the object.
	 *
	 * $key can be string or array.
	 * If $key is string, the attribute value will be overwritten by $value.
	 * If $key is an array, it will overwrite all the data in the object.
	 *
	 * @param string|array $key
	 * @param [mixed] $value
	 * @return ZeusLib_Object
	 */
	public function setData($key, $value = null)
	{
		// Set data changed flag
		$this->hasDataChanged = true;
		
		// Check whether the key is a string or a number
		if(is_string($key) || is_numeric($key))
		{
			// Add/overwrite existing data
			$this->data[$key] = $value;
		}
		// Check whether $key is an array
		elseif(is_array($key))
		{
			// Set the given array to the object data
			$this->data = $key;
		}
		// Check whether $key is a ZeusLib_Object
		elseif($key instanceof ZeusLib_Object)
		{
			// Set the data
			$this->data = $key->getData();
			$this->hasDataChanged = $key->dataChanged();
			$this->isDeleted = $key->isDeleted();
		}
		// The key is something else and so something not wanted
		else {
			// Throw exception for an unexpected type of the key
			throw new Exception("Unexpected key type.");
		}

		// Return instance
		return $this;
	}

	/**
	 * Adds data to an array in the object.
	 * TODO: e.g. and more information
	 *
	 * @param string $key
	 * @param mixed  $value
	 * @param mixed  $arrayIndex
	 * @return ZeusLib_Object
	 */
	public function addDataToArray($key, $value, $arrayIndex = false)
	{
		// Set data changed flag
		$this->dataChanged(true);

		// Check whether the key not exists
		if(!$this->hasData($key))
		{
			// Create an array
			$this->setData($key, array());
		}

		// Get the array for the key
		$array = $this->getData($key);

		// Check whether we want to give an extra index
		if($arrayIndex === false)
		{
			// Append the data
			$array[] = $value;
		}
		// We dont want to give an extra index
		else
		{
			// Append the data with index
			$array[$arrayIndex] = $value;
		}

		// Save the array
		$this->setData($key, $array);

		// Return instance
		return $this;
	}

	/**
	 * Gets data from the object.
	 *
	 * If $key is not set it returns all data.
	 * If $key is set it returns the data for the given key if exists or false.
	 *
	 * @param [mixed] $key
	 * @return mixed|boolean|array
	 */
	public function getData($key = null)
	{
		// Check whether no key is given
		if($key === null)
		{
			// Return all data
			return $this->data;
		}

		// Check whether the data for the key not exists
		if(!$this->hasData($key))
		{
			// Return null
			return null;
		}

		// Return the data for the key
		return $this->data[$key];
	}

	/**
	 * Checks whether the object contains data.
	 *
	 * If $key is not set it returns whether the object has some data
	 * If $key is set it return whether the object contains the data for the index
	 *
	 * @param [mixed] $key
	 * @return boolean
	 */
	public function hasData($key = null)
	{
		// Check whether no key is given
		if($key === null)
		{
			// Count the data array
			$count = count($this->data);

			// Return if there is data
			return $count > 0;
		}

		// Return whether the data for the given index exists
		return isset($this->data[$key]);
	}

	/**
	 * Unsets data from the object.
	 *
	 * If $key is not set it deletes all data
	 * If $key is set it deletes data for the given index
	 *
	 * @param [mixed] $key
	 * @return ZeusLib_Object
	 */
	public function unsetData($key = null)
	{
		// Set data changed flag
		$this->hasDataChanged = true;

		// Check whether no key is given
		if($key === null)
		{
			// Reset the data
			$this->data = array();
		}

		// Unset data for the given index
		unset($this->data[$key]);

		// Return instance
		return $this;
	}

	/**
	 * Sets or gets the data changed flag if a param is given or not.
	 *
	 * @param [boolean] $flag
	 * @return boolean|ZeusLib_Object
	 */
	public function dataChanged($flag = null)
	{
		// Check whether no flag was given
		if($flag === null)
		{
			// Return current data changed flag
			return $this->hasDataChanged;
		}

		// Set data changed flag
		$this->hasDataChanged = (bool)$flag;

		// Return instance
		return $this;
	}

	/**
	 * Set, Get, Uns and Has attribute wrapper for implicit object access.
	 *
	 * @param   string $method
	 * @param   array  $args
	 * @return  mixed
	 */
	public function __call($method, $args)
	{
		// Switch the method type
		switch (substr($method, 0, 3))
		{
			// Getter
			case 'get' :
				$key = $this->_underscore(substr($method, 3));
				return $this->getData($key, isset($args[0]) ? $args[0] : null);
				break;

			// Setter
			case 'set' :
				$key = $this->_underscore(substr($method, 3));
				return $this->setData($key, isset($args[0]) ? $args[0] : null);
				break;

			// Unsetter
			case 'uns' :
				$key = $this->_underscore(substr($method, 3));
				return $this->unsetData($key);
				break;
				
			// Has'er
			case 'has' :
				$key = $this->_underscore(substr($method, 3));
				return $this->hasData($key);
				break;
		}

		// Throw new exception
		throw new Exception("Undefined method called.");
	}


	/**
	 * Converts field names for setters and getters.
	 *
	 * $this->setMyField($value) === $this->setData('my_field', $value)
	 * Uses cache to eliminate unneccessary preg_replace
	 *
	 * @param string $name
	 * @return string
	 */
	protected function _underscore($name)
	{
		// Check whether the name already exists
		if (isset(self::$_underscoreCache[$name]))
		{
			// Return the cache value
			return self::$_underscoreCache[$name];
		}

		// Generate the underscored value
		$result = strtolower(preg_replace('/(.)([A-Z])/', "$1_$2", $name));

		// Write to underscore cache
		self::$_underscoreCache[$name] = $result;

		// Return the result
		return $result;
	}

	protected function _serialize()
	{
		// Return the serialized data
		return serialize($this);
	}

	protected function _unserialize($str)
	{
		// Return the unserialized data
		return unserialize($str);
	}

	/**
	 * Implements the getIterator function of the IteratorAggregate interface
	 * to supply an object iteration.
	 *
	 * @return Iterator
	 */
	public function getIterator()
	{
		// Create an ArrayObject
		$arrayObject = new ArrayObject($this->data);

		// Get an Iterator instance
		return $arrayObject->getIterator();
	}

}